﻿using System.Globalization;
using System.Web;

namespace FilteredPageReference
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using EPiServer;
    using EPiServer.Core;
    using EPiServer.Core.PropertySettings;
    using EPiServer.DataAbstraction;

    /// <summary>
    /// Helper class
    /// </summary>
    public static class Helper
    {

        private static bool _attemptedToFindTypedPageDataType;
        private static Type _typedPageDataType;
        private static Type _pageTypeResolverType;
        private static PropertyInfo _pageTypeResolverInstanceProperty;
        private static MethodInfo _getPageTypeTypeMethodInfo;
        private static MethodInfo _getPageTypeIdMethodInfo;

        /// <summary>
        /// Determines whether the page is valid
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="settingsContainer">The settings container.</param>
        /// <param name="propertyInformation">The property information.</param>
        /// <returns>
        ///   <c>true</c> if the page is valid; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsValidPage(object value, PropertySettingsContainer settingsContainer, PropertyInformation propertyInformation)
        {
            if (settingsContainer == null)
                return true;

            bool valid = false;

            if (value is PageReference && !PageReference.IsNullOrEmpty(value as PageReference))
            {
                PropertySettingsWrapper settingsWrapper = settingsContainer.GetSetting(typeof(PropertyFilteredPageReferenceSettings));

                if (settingsWrapper != null)
                {
                    PropertyFilteredPageReferenceSettings settings = settingsWrapper.PropertySettings as PropertyFilteredPageReferenceSettings;

                    if (settings != null)
                    {
                        try
                        {
                            PageData page = DataFactory.Instance.GetPage(value as PageReference);
                            valid = IsValidPageType(page.PageTypeID, settings, propertyInformation);
                        }
                        catch (PageNotFoundException)
                        {
                            valid = false;
                        }
                    }
                    else
                        valid = true;
                }
                else
                    valid = true;
            }

            return valid;
        }

        /// <summary>
        /// Determines whether page type builder is being used
        /// </summary>
        /// <returns>True if page type builder is being used, otherwise false</returns>
        public static bool UsingPageTypeBuilder()
        {
            if (_typedPageDataType == null && !_attemptedToFindTypedPageDataType)
            {
                _typedPageDataType = Type.GetType("PageTypeBuilder.TypedPageData");
                _attemptedToFindTypedPageDataType = true;
            }

            return _typedPageDataType != null;
        }

        /// <summary>
        /// Gets the Typed page data type
        /// </summary>
        /// <returns>TypePageData type</returns>
        public static Type GetTypedPageDataType()
        {
            if (!UsingPageTypeBuilder())
                return null;

            return _typedPageDataType;
        }

        private static PropertyInfo GetPageTypeResolverInstance()
        {
            if (!UsingPageTypeBuilder())
                return null;

            if (_pageTypeResolverInstanceProperty != null)
                return _pageTypeResolverInstanceProperty;

            if (_pageTypeResolverType == null)
                _pageTypeResolverType = Type.GetType("PageTypeBuilder.PageTypeResolver");

            if (_pageTypeResolverType == null)
                return null;

            _pageTypeResolverInstanceProperty = _pageTypeResolverType.GetProperty("Instance");
            return _pageTypeResolverInstanceProperty;
        }

        /// <summary>
        /// Gets the type of the page type.
        /// </summary>
        /// <param name="pageTypeId">The page type id.</param>
        /// <returns>Page type type</returns>
        public static Type GetPageTypeType(int pageTypeId)
        {
            PropertyInfo instanceProperty = GetPageTypeResolverInstance();

            if (instanceProperty == null)
                return null;

            object instance = instanceProperty.GetValue(null, null);

            if (instance == null)
                return null;

            if (_getPageTypeTypeMethodInfo == null)
                _getPageTypeTypeMethodInfo = instance.GetType().GetMethod("GetPageTypeType", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(int) }, null);

            if (_getPageTypeTypeMethodInfo == null)
                return null;

            return _getPageTypeTypeMethodInfo.Invoke(instance, new object[] { pageTypeId }) as Type;
        }

        /// <summary>
        /// Gets the page type ID of a page type builder type.
        /// </summary>
        /// <param name="pageType">Type of the page.</param>
        /// <returns></returns>
        public static int? GetPageTypeID(Type pageType)
        {
            PropertyInfo instanceProperty = GetPageTypeResolverInstance();

            if (instanceProperty == null)
                return null;

            object instance = instanceProperty.GetValue(null, null);

            if (instance == null)
                return null;

            if (_getPageTypeIdMethodInfo == null)
                _getPageTypeIdMethodInfo = instance.GetType().GetMethod("GetPageTypeID", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(Type) }, null);

            if (_getPageTypeIdMethodInfo == null)
                return null;

            return (int?)_getPageTypeIdMethodInfo.Invoke(instance, new object[] { pageType });
        }

        /// <summary>
        /// Determines whether the assembly or type name is valid
        /// </summary>
        /// <param name="fullName">The full name.</param>
        /// <returns>
        ///   <c>true</c> if the name is valid; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsValidAssemblyOrTypeName(string fullName, bool isAssembly)
        {
            if (isAssembly)
            {
                return Configuration.FilteredPageReferenceConfiguration.GetConfiguration()
                    .ValidAssemblyNamePrefixes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
                    .Any(current => fullName.StartsWith(current, StringComparison.OrdinalIgnoreCase));
            }
            else
            {
                return Configuration.FilteredPageReferenceConfiguration.GetConfiguration()
                    .ValidFullTypeNamePrefixes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
                    .Any(current => fullName.StartsWith(current, StringComparison.OrdinalIgnoreCase));
            }
        }

        /// <summary>
        /// Gets the allowed page types for picking.
        /// </summary>
        /// <param name="settings">The settings.</param>
        /// <param name="propertyInformation">The property information.</param>
        /// <returns></returns>
        private static AllowedPageTypes GetAllowedPageTypesForPicking(PropertyFilteredPageReferenceSettings settings, PropertyInformation propertyInformation)
        {
            string key = string.Format(CultureInfo.InvariantCulture, "{0}_{1}_{2}_{3}_{4}_{5}",
                                       propertyInformation.PageId,
                                       propertyInformation.WorkId,
                                       propertyInformation.LanguageId,
                                       propertyInformation.PropertyName,
                                       propertyInformation.PageTypeId,
                                       propertyInformation.ParentPageId);

            if (HttpContext.Current != null && HttpContext.Current.Items[key] != null)
                return HttpContext.Current.Items[key] as AllowedPageTypes;

            AllowedPageTypes allowedPageTypes = new AllowedPageTypes
                                                                      {
                                                                          AllowedPageTypesAndInterfaces = settings.AllowedPageTypesAndInterfaces,
                                                                          PageTypeNames = settings.PageTypeNames.Split(new[] { ','}, StringSplitOptions.RemoveEmptyEntries).ToList(),
                                                                          PropertyInformation = propertyInformation
                                                                      };

            if (PropertyFilteredPageReferenceSettings.GetAllowedPageTypes != null)
                PropertyFilteredPageReferenceSettings.GetAllowedPageTypes.Invoke(allowedPageTypes);

            if (HttpContext.Current != null)
                HttpContext.Current.Items.Add(key, allowedPageTypes);

            return allowedPageTypes;
        }

        /// <summary>
        /// Determines whether the page is a valid page type.
        /// </summary>
        /// <param name="pageTypeId">The page type id.</param>
        /// <param name="settings">The settings.</param>
        /// <param name="propertyInformation">The property information.</param>
        /// <param name="allowedPageTypes">The allowed page types.</param>
        /// <returns>
        ///   <c>true</c> if the page is a valid page type; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsValidPageType(int pageTypeId, PropertyFilteredPageReferenceSettings settings, PropertyInformation propertyInformation)
        {
            List<string> fullTypeNames = new List<string>();
            AllowedPageTypes allowedPageTypes = GetAllowedPageTypesForPicking(settings, propertyInformation);

            if (!string.IsNullOrEmpty(allowedPageTypes.FullTypeNames) && UsingPageTypeBuilder())
            {
                bool valid = false;
                Type pageTypeType = GetPageTypeType(pageTypeId);

                if (pageTypeType != null)
                {
                    fullTypeNames = allowedPageTypes.FullTypeNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();

                    foreach (string fullTypeName in fullTypeNames)
                    {
                        Type type = Type.GetType(fullTypeName);

                        if (type == null)
                            throw new Exception(string.Format("Type '{0}' does not exist.", fullTypeName));

                        valid = pageTypeType.Equals(type) || pageTypeType.IsSubclassOf(type) || (type.IsInterface && pageTypeType.GetInterfaces().Where(current => current.Equals(type)).Count() > 0);

                        if (valid)
                            break;
                    }

                    if (valid)
                        return true;
                }
                else
                    fullTypeNames.AddRange(allowedPageTypes.FullTypeNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
            }

            List<string> allPageTypeNames = allowedPageTypes.PageTypeNames;

            if (allPageTypeNames.Count > 0)
            {
                PageType pageType = PageType.Load(pageTypeId);
                return allPageTypeNames.Where(current => string.Equals(current.Trim(), pageType.Name.Trim(), StringComparison.OrdinalIgnoreCase)).Count() > 0;
            }

            return fullTypeNames.Count == 0 && allPageTypeNames.Count == 0;
        }

        /// <summary>
        /// Gets the comma delimited list of allowed page types
        /// </summary>
        /// <param name="pageTypeNames">The page type names.</param>
        /// <param name="fullTypeNames">The full type names.</param>
        /// <returns>Comma delimited list of allowed page types</returns>
        public static string GetCommaDelimitedListOfAllowedPageTypes(string pageTypeNames, string fullTypeNames)
        {
            // need to order by name and dedupe
            List<string> allowedPageTypeNames = new List<string>();

            if (!string.IsNullOrEmpty(fullTypeNames) && UsingPageTypeBuilder())
            {
                List<string> fullTypeNamesList = fullTypeNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
                List<Type> allPageTypeBuilderTypes = GetPageTypeBuilderTypes();
                List<Type> pageTypeBuilderTypes = allPageTypeBuilderTypes.Where(current => fullTypeNamesList.Contains(current.FullName)).ToList();

                foreach (Type type in pageTypeBuilderTypes)
                {
                    if (type.IsInterface)
                    {
                        Type currentType = type;
                        List<Type> typesWhichImplementInterface = allPageTypeBuilderTypes.Where(current => current.GetInterfaces().Where(currentInterface => currentInterface.Equals(currentType)).Count() > 0).ToList();

                        foreach (Type childType in typesWhichImplementInterface)
                            AddPageTypeNameToListIfValid(childType, allowedPageTypeNames);
                    }
                    else
                    {
                        AddPageTypeNameToListIfValid(type, allowedPageTypeNames);
                        Type currentType = type;
                        List<Type> typesWhichImplementType = allPageTypeBuilderTypes.Where(current => current.IsSubclassOf(currentType)).ToList();

                        foreach (Type childType in typesWhichImplementType)
                            AddPageTypeNameToListIfValid(childType, allowedPageTypeNames);
                    }
                }
            }

            if (!string.IsNullOrEmpty(pageTypeNames))
            {
                List<string> pageTypeNamesList = pageTypeNames.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();

                foreach (string pageTypeName in pageTypeNamesList.Where(pageTypeName => !allowedPageTypeNames.Contains(pageTypeName)))
                    allowedPageTypeNames.Add(pageTypeName);
            }

            allowedPageTypeNames.Sort();
            pageTypeNames = allowedPageTypeNames.Aggregate(string.Empty, (current, pageTypeName) => current + (pageTypeName + ", "));
            return pageTypeNames.Trim(new[] { ',', ' ' });
        }

        /// <summary>
        /// Gets the page type builder types.
        /// </summary>
        /// <returns>Type list of page type builder types and interfaces</returns>
        public static List<Type> GetPageTypeBuilderTypes()
        {
            List<Type> pageTypeBuilderTypes = new List<Type>();
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            Type typedPageDataType = GetTypedPageDataType();

            foreach (Assembly assembly in assemblies.Where(current => IsValidAssemblyOrTypeName(current.GetName().Name, true)))
            {
                try
                {
                    foreach (Type type in assembly.GetTypes().Where(current => IsValidAssemblyOrTypeName(current.FullName, false) && current.IsPublic))
                    {
                        //if (type.IsInterface || (typedPageDataType != null && type.IsSubclassOf(typedPageDataType)))
                        if (typedPageDataType != null && type.IsSubclassOf(typedPageDataType))
                        {
                            if (!pageTypeBuilderTypes.Contains(type))
                                pageTypeBuilderTypes.Add(type);

                            foreach (Type interfaceType in type.GetInterfaces().Where(current => Helper.IsValidAssemblyOrTypeName(current.FullName, false)))
                            {
                                if (!pageTypeBuilderTypes.Contains(interfaceType))
                                    pageTypeBuilderTypes.Add(interfaceType);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    string message = ex.Message;
                }
            }

            return pageTypeBuilderTypes;
        }

        /// <summary>
        /// Gets the property settings.
        /// </summary>
        /// <param name="pageIdWorkIdLanguageBranchAndPropertyName">Name of the page id work id language branch and property.</param>
        /// <param name="sessionKeyName">Name of the session key.</param>
        /// <returns>
        /// PropertyFilteredPageReferenceSettings instance
        /// </returns>
        public static PropertyFilteredPageReferenceSettings GetPropertySettings(string pageIdWorkIdLanguageBranchAndPropertyName, string sessionKeyName)
        {
            bool propertyInformationValid = true;

             PropertyInformation propertyInformation = GetPropertyInformation(pageIdWorkIdLanguageBranchAndPropertyName, ref propertyInformationValid);

            if (!propertyInformationValid)
                return new PropertyFilteredPageReferenceSettings();

            if (!string.IsNullOrEmpty(sessionKeyName) && HttpContext.Current.Session[sessionKeyName] != null)
            {
                PropertyFilteredPageReferenceSettings settings = HttpContext.Current.Session[sessionKeyName] as PropertyFilteredPageReferenceSettings;
                settings.CurrentPageId = propertyInformation.PageId;
                return settings;
            }

            PageType pageType = PageType.Load(propertyInformation.PageTypeId);
            PageDefinition definition = pageType.Definitions.Where(current => string.Equals(current.Name, propertyInformation.PropertyName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

            if (definition != null)
            {
                PropertyData property = definition.CreatePropertyData();

                if (property.SettingsContainer != null)
                {
                    PropertySettingsWrapper settingsWrapper = property.SettingsContainer.GetSetting(typeof(PropertyFilteredPageReferenceSettings));

                    if (settingsWrapper != null)
                    {
                        PropertyFilteredPageReferenceSettings settings = settingsWrapper.PropertySettings as PropertyFilteredPageReferenceSettings;

                        if (settings != null)
                        {
                            if (!UsingPageTypeBuilder())
                                settings.FullTypeNames = string.Empty;

                            settings.CurrentPageId = propertyInformation.PageId;

                            return settings;
                        }
                    }
                }
            }

            return new PropertyFilteredPageReferenceSettings();
        }

        /// <summary>
        /// Gets the property information.
        /// </summary>
        /// <param name="pageIdWorkIdLanguageBranchAndPropertyName">Name of the page id work id language branch and property.</param>
        /// <param name="valid">if set to <c>true</c> [valid].</param>
        /// <returns>PropertyInformation instance</returns>
        public static PropertyInformation GetPropertyInformation(string pageIdWorkIdLanguageBranchAndPropertyName, ref bool valid)
        {
            string[] parts = pageIdWorkIdLanguageBranchAndPropertyName.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

            if (parts.Length != 6)
            {
                valid = false;
                return new PropertyInformation();
            }

            string pageId = parts[0];
            int workId = int.Parse(parts[1]);
            string languageId = parts[2].Trim();
            int pageTypeId = int.Parse(parts[3].Trim());
            string parentPageId = parts[4].Trim();
            string propertyName = parts[5].Trim();

            if (propertyName.Contains("?"))
                propertyName = propertyName.Substring(0, propertyName.IndexOf("?")).Trim();

            if (propertyName.Contains("&"))
                propertyName = propertyName.Substring(0, propertyName.IndexOf("&")).Trim();

            return new PropertyInformation
                       {
                           PageId = pageId,
                           WorkId = workId,
                           LanguageId = languageId,
                           PropertyName = propertyName,
                           PageTypeId = pageTypeId,
                           ParentPageId = parentPageId
                       };
        }

        public static PageReference GetPagePickingStartPage(PropertyFilteredPageReferenceSettings settings, string fprData)
        {
            bool sourceSet = false;
            PageReference startPage = PageReference.EmptyReference;

            // If a start from delegate has been defined then invoke it
            if (PropertyFilteredPageReferenceSettings.GetPagePickingStartPage != null)
            {
                bool valid = true;
                PropertyInformation propertyInformation = Helper.GetPropertyInformation(fprData, ref valid);
                PagePickingStartPage pagePickingStartPage = new PagePickingStartPage { PropertyInformation = propertyInformation };

                PropertyFilteredPageReferenceSettings.GetPagePickingStartPage.Invoke(pagePickingStartPage);

                if (!PageReference.IsNullOrEmpty(pagePickingStartPage.StartPage))
                {
                    startPage = pagePickingStartPage.StartPage;
                    sourceSet = true;
                }
            }

            if (!sourceSet)
            {
                if (PageReference.IsNullOrEmpty(settings.StartFrom) && !(settings.StartFromCurrentPage && String.IsNullOrEmpty(settings.CurrentPageId)) && !settings.StartFromCurrentSiteStartPage)
                    startPage = PageReference.RootPage;
                else
                {
                    if (settings.StartFromCurrentSiteStartPage)
                        startPage = PageReference.StartPage;
                    else if (settings.StartFromCurrentPage && !String.IsNullOrEmpty(settings.CurrentPageId))
                        startPage = PageReference.Parse(settings.CurrentPageId);
                    else
                        startPage = settings.StartFrom;
                }
            }

            return startPage;
        }

        public static bool IsDescendent(PageReference ancestorPageLink, PageData descendent)
        {
            if (ancestorPageLink.ID == descendent.PageLink.ID)
                return true;

            PageReference parentPageReference = descendent.ParentLink;

            while (!PageReference.IsNullOrEmpty(parentPageReference))
            {
                descendent = DataFactory.Instance.GetPage(parentPageReference);

                if (ancestorPageLink.ID == descendent.PageLink.ID)
                    return true;

                parentPageReference = descendent.ParentLink;
            }

            return false;
        }

        /// <summary>
        /// Adds the page type name to list if valid.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="allowedPageTypeNames">The allowed page type names.</param>
        private static void AddPageTypeNameToListIfValid(Type type, List<string> allowedPageTypeNames)
        {
            int? pagetypeId = GetPageTypeID(type);

            if (!pagetypeId.HasValue)
                return;

            string pageTypeName = PageType.Load(pagetypeId.Value).Name;

            if (!allowedPageTypeNames.Contains(pageTypeName))
                allowedPageTypeNames.Add(pageTypeName);
        }

    }
}
